<template>
  <div
    v-if="doc"
    ref="viewerEl"
    class="fa-contract fa-contract-scope"
    :class="{ 'has-riding': layout.ridingActive, 'has-toolbar': toolbar }"
    v-click-outside="() => (state.activated = false)"
    @click="state.activated = true"
  >
    <div class="fa-contract__viewer">
      <div ref="scroller" class="fa-contract__scroll" @scroll.passive="onScroll">
        <div class="fa-contract__doc-wrapper" :style="{ width: layout.docAreaZoomWidth + 'px' }">
          <div
            class="fa-contract__doc"
            :style="{
              width: `${layout.docAreaWidth}px`,
              height: `${layout.docAreaHeight}px`,
              marginLeft: `-${layout.docAreaWidth / 2}px`,
              transform: `scale(${zoom.level})`
            }"
          >
            <ContractPage
              v-for="page of scroll.visiblePages"
              :key="`${state.doc.id}-${page.p}`"
              :page="page"
              :doc="state.doc"
              :cumulative-height="state.cumulativeHeights[page.p]"
              @click.native="$emit('page-click', { doc, page, p: page.p })"
            >
              <slot name="page" v-bind="{ doc, page }" />
            </ContractPage>

            <slot name="doc" v-bind="{ doc }" />
          </div>
        </div>
      </div>

      <!-- 左侧 -->
      <div
        class="fa-contract__tools tools-left"
        :class="[layout.isToolsYFixed ? 'fixed-scroller' : 'fixed-doc']"
        :style="tools.style.left"
      >
        <div class="tools-wrapper tools-wrapper__left-top">
          <slot name="tools-left-top" />
        </div>

        <div class="tools-wrapper tools-wrapper__left-middle">
          <slot name="tools-left-middle" />
        </div>

        <div class="tools-wrapper tools-wrapper__left-bottom">
          <slot name="tools-left-bottom" />
        </div>
      </div>

      <!-- 右侧 -->
      <div
        class="fa-contract__tools tools-right"
        :class="[layout.isToolsYFixed ? 'fixed-scroller' : 'fixed-doc']"
        :style="tools.style.right"
      >
        <div class="tools-wrapper tools-wrapper__right-top">
          <slot name="tools-right-top" />
        </div>

        <div class="tools-wrapper tools-wrapper__right-middle">
          <!-- 骑缝控件 -->
          <RidingButton v-if="riding && state.pageCount !== 1" :active.sync="layout.ridingActive" />

          <slot name="tools-right-middle" />
        </div>

        <div class="tools-wrapper tools-wrapper__right-bottom">
          <div class="btn-scroll__wrapper tools-btn">
            <!--回到顶部-->
            <a
              v-show="tools.scrollTopVisible"
              class="btn-scroll fa-icon-scroll-top"
              @click="() => scroll.scrollToTop()"
            ></a>

            <!--回到底部-->
            <a
              v-show="tools.scrollBottomVisible"
              class="btn-scroll fa-icon-scroll-bottom"
              @click="() => scroll.scrollToBottom()"
            ></a>
          </div>

          <slot name="tools-right-bottom" />
        </div>
      </div>

      <slot />
    </div>

    <ContractToolbar
      v-if="toolbar"
      :page-index.sync="scroll.pageIndex"
      :page-count="state.pageCount"
      :zoom="zoom"
      :levels="zoomLevels"
    >
      <template #left><slot name="toolbar-left" /></template>
      <template #right><slot name="toolbar-right" /></template>
    </ContractToolbar>

    <ContractPopovers />
  </div>

  <div v-else class="fa-contract"></div>
</template>

<script lang="ts">
import {
  computed,
  defineComponent,
  ref,
  toRef,
  toRefs,
  watch,
  PropType,
  provide,
  getCurrentInstance,
  nextTick,
  onUnmounted
} from 'vue'
import { MessageBox } from 'element-ui'
import ContractPage from './ContractPage.vue'
import ContractToolbar from './ContractToolbar.vue'
import ContractPopovers from './ContractPopovers.vue'
import RidingButton from '../riding-area/RidingButton.vue'

import config from '@/config'
import {
  Viewer,
  DocData,
  PageData,
  WidgetType,
  SignData,
  SignOptions,
  WidgetSource,
  WidgetSources,
  WidgetSourceItem
} from '@/types'
import { EffectLock, clickOutside, useScrollState } from '@/utils'
import { Widget } from '@/models'

import emits from './emits'

import useDocState from './useDocState'
import useDocScroll from './useDocScroll'
import useZoom from './useZoom'
import useLayout from './useLayout'
import useTools from './useTools'

import useWidgetActive from './useWidgetActive'
import useWidgetSort from './useWidgetSort'
import useWidgetOperate from './useWidgetOperate'
import useWidgetValidate from './useWidgetValidate'
import useWidgetSource from './useWidgetSource'
import useSelection from './useSelection'
import useHotkeys from './hotkey/useHotkeys'
import useWidgetName from './useWidgetName'

/** 文档查看 */
export default defineComponent({
  name: 'FaContract',

  components: { ContractPage, RidingButton, ContractToolbar, ContractPopovers },

  directives: { clickOutside },

  props: {
    /**
     * 文档展示
     */

    /** 文档列表 - 用于批量文档 */
    docs: {
      type: Array as PropType<DocData[]>,
      default: () => []
    },

    /** 当前文档索引，需要 `.sync` 双向同步 */
    docIndex: {
      type: Number,
      default: 0
    },

    /**
     * 页面图片地址
     * @description 如果文档页面数据 `PageData` 未设置 `src`，则可使用该选项统一设置文档图片地址
     * @version v1.4.0
     */
    pageSrc: Function as PropType<(doc: DocData, page: PageData) => string | void>,

    /**
     * 页面 HTML 内容
     * @description 如未设置页面图片，可使用该方法设置每页 HTML 内容，支持异步
     */
    pageHtml: Function as PropType<(doc: DocData, page: PageData) => string | Promise<string> | void>,

    /** 页面上下间距 (px) */
    pageSpacing: {
      type: Number,
      default: () => config.page.spacing
    },

    /** 是否开启工具栏 */
    toolbar: {
      type: Boolean,
      default: true
    },

    /** 工具栏缩放级别列表 */
    zoomLevels: {
      type: Array as PropType<number[]>,
      default: () => [...config.zoomLevels]
    },

    /**
     * 控件配置
     */

    /** 是否启用控件 */
    widget: {
      type: Boolean,
      default: false
    },

    /** 控件列表 */
    widgets: {
      type: Array as PropType<Widget[]>,
      default: () => []
    },

    /** 是否允许拖入控件 */
    droppable: {
      type: Boolean,
      default: true
    },

    /** 控件自定义类名 */
    widgetClassName: Function as PropType<(widget: Widget) => string | void>,

    /** 控件自定义提示 */
    widgetTips: Function as PropType<(widget: Widget) => string | void>,

    /** 控件是否支持跨页拖动（仅限普通控件，骑缝和批量控件不允许跨页拖动） */
    widgetMovingAcrossPages: {
      type: Boolean,
      default: true
    },

    /** 控件名称是否必填 */
    widgetNameRequired: {
      type: [Boolean, Function] as PropType<boolean | ((widget: Widget) => boolean)>,
      default: true
    },

    /** 控件参与方是否必须选择 */
    widgetActorRequired: {
      type: [Boolean, Function] as PropType<boolean | ((widget: Widget) => boolean)>,
      default: true
    },

    /** 控件数据来源 */
    widgetSources: {
      type: Function as PropType<undefined | ((widget: Widget) => WidgetSourceItem[] | void)>,

      /** 控件数据来源 默认 */
      default: (widget: Widget) => {
        // 仅单行文本、身份证控件
        if (![WidgetType.INPUT, WidgetType.ID].includes(widget.type)) return

        // 身份证
        if (widget.type === WidgetType.ID) {
          return WidgetSources.filter(({ key }) => key === WidgetSource.CERTIFIED_ID)
        }

        if (widget.actor.isPerson) {
          // 排除企业
          return WidgetSources.filter(({ key }) => !key.includes('CORP_'))
        }

        return WidgetSources
      }
    },

    /** 控件数据来源加载 */
    widgetSourceLoader: {
      type: Function as PropType<(widgets: Widget[]) => Promise<Record<WidgetSource | string, string> | void>>
    },

    /** 控件数据来源是否自动加载 */
    widgetSourceAutoLoad: {
      type: Boolean,
      default: true
    },

    /** 控件拖入前，可返回 `Promise.reject` 阻止 */
    beforeWidgetDrop: Function as PropType<(widget: Widget) => void>,

    /** 控件删除前，可返回 `Promise.reject` 阻止 */
    beforeWidgetRemove: {
      type: Function as PropType<(...widgets: Widget[]) => void>,
      /** 控件删除前 */
      default: async (...widgets: Widget[]) => {
        if (widgets.length > 1) {
          await MessageBox.confirm('是否删除所选控件？', '操作确认')
        }

        if (widgets.some(w => w.isBatch || w.linkWidgets.length)) {
          await MessageBox.confirm('删除该控件将同步删除同批关联的所有控件，确定删除吗？', '提示')
        }
      }
    },

    /**
     * 签署控件
     */

    /**
     * 签章配置选项
     * @description 可配置日期章图片地址、日期章全局配置、签章全局切换、签章自定义 class 等
     * @version v1.4.0
     */
    signOptions: {
      type: Object as PropType<SignOptions>,
      default: () => ({})
    },

    /** 是否启用骑缝控件 */
    riding: {
      type: Boolean,
      default: false
    },

    /** 允许的骑缝控件类型 */
    allowedRidingTypes: {
      type: Array as PropType<WidgetType[]>,
      default: () => [WidgetType.SEAL]
    },

    /** 签名列表 - 用于签名切换 */
    signatures: {
      type: Array as PropType<SignData[]>,
      default: () => []
    },

    /** 印章列表 - 用于印章切换 */
    seals: {
      type: Array as PropType<SignData[]>,
      default: () => []
    },

    /** 签章切换前，可返回 `Promise.reject` 阻止 */
    beforeSignChange: Function as PropType<(widget: Widget, sign: SignData) => void>,

    /**
     * 填写控件
     */

    /** 附件上传方法 */
    attachmentUpload: Function as PropType<(file: File, widget: Widget) => Promise<{ id: string; fileName: string }>>,

    /** 附件接受上传的文件类型 */
    attachmentAccept: String,

    /** 图片控件上传方法 */
    imageUpload: Function as PropType<
      (file: File, widget: Widget) => Promise<{ id: string; name?: string; src?: string }>
    >,

    /** 图片控件接受上传的文件类型 */
    imageAccept: {
      type: String,
      default: '.jpg, .jpeg, .png'
    },

    /**
     * 图片控件地址
     * @description 如果图片控件为设置 `image.src`，则可使用该选项统一设置图片控件地址
     */
    imageSrc: Function as PropType<(widget: Widget) => string | void>,

    /**
     * 图片上传前是否裁剪
     */
    imageCrop: {
      type: Boolean,
      default: true
    }
  },

  emits,

  setup(props, { emit }) {
    /** 文档查看实例 */
    const viewer = getCurrentInstance()?.proxy as Viewer

    /** 节点 */
    const viewerEl = ref<null | HTMLElement>(null)

    /** 控件拖拽和调整大小锁 */
    const DnsLocker = new EffectLock()

    provide('Viewer', viewer)

    provide('DnsLocker', DnsLocker)

    const { widgets, pageSpacing, zoomLevels, beforeWidgetRemove } = toRefs(props)

    /** 文档列表 */
    const docs = toRef(props, 'docs')

    /** 文档索引 */
    const docIndex = computed({
      set: (index: number) => emit('update:docIndex', index),
      get: () => props.docIndex
    })

    /** 文档状态 */
    const state = useDocState(docs, docIndex)

    /** 当前文档 */
    const doc = toRef(state, 'doc')

    /** 骑缝控件列表(包含所有文档) */
    const ridings = computed(() => widgets.value.filter(item => item.isRiding))

    /** 当前文档骑缝控件列表 */
    const docRidings = computed(() => {
      const { id } = doc.value
      return ridings.value.filter(item => item.docId === id)
    })

    /** 骑缝区域宽度 */
    const ridingWidth = computed(() => {
      const ridingWidths = docRidings.value.map(w => w.w)
      const defaultWidth = config.riding.areaDefaultWidth

      return ridingWidths.length ? Math.max(...ridingWidths, defaultWidth) : defaultWidth
    })

    /** 批量控件列表(包含所有文档) */
    const batches = computed(() => widgets.value.filter(item => item.isBatch))

    /** 滚动状态 */
    const scrollState = useScrollState()

    /** 滚动节点 */
    const scroller = toRef(scrollState, 'scroller')

    /** 缩放 */
    const zoom = useZoom(zoomLevels)

    /** 缩放事件 */
    watch(
      () => zoom.level,
      level => emit('zoom', level)
    )

    /** 布局 */
    const layout = useLayout({
      scrollState,
      doc,
      state,
      zoom,
      ridingWidth,
      pageSpacing
    })

    /** 工具区域 */
    const tools = useTools({ layout, scrollState })

    /** 文档滚动 */
    const scroll = useDocScroll({
      state,
      scrollState,
      zoom,
      layout,
      widgets
    })

    /** 控件操作 */
    const widgetActive = useWidgetActive(widgets)

    /** 控件排序 */
    const widgetSort = useWidgetSort(widgets, docs)

    /** 控件操作 */
    const widgetOperate = useWidgetOperate(widgets, emit, widgetActive, scroll.scrollToWidget, beforeWidgetRemove)

    /** 控件校验 */
    const widgetValidate = useWidgetValidate(widgets, widgetOperate, widgetSort)

    /** 控件数据来源 */
    const source = useWidgetSource(viewer)

    /** 控件多选 */
    const selection = useSelection(selected => emit('widget-select', selected))

    /** 快捷按键 */
    useHotkeys(viewer, viewerEl, emit)

    /** 控件初始命名 */
    useWidgetName(viewer, widgets)

    /** 文档切换滚动到第一页 */
    watch(
      () => doc.value?.id,
      async () => {
        scrollState.scrollTop = 0

        await nextTick()

        scroll.scrollToPage(0, false)
      }
    )

    /** 缩放后还原滚动视图区域 */
    watch(
      () => zoom.level,
      async () => {
        const wrap = scroller.value

        if (!wrap) return 0

        // 滚动区域比例
        const ratioY = (wrap.scrollTop + wrap.clientHeight / 2) / wrap.scrollHeight

        await nextTick()

        // 按比例还原视图滚动位置
        const y = ratioY * wrap.scrollHeight - wrap.clientHeight / 2

        scroll.scrollTo(y, null, false)
      }
    )

    /** 控件绑定合同控件 */
    watch(
      () => props.widgets.filter(item => !item.viewer),
      list => {
        list.forEach(item => (item.viewer = viewer))
      },
      { immediate: true }
    )

    /** 激活的控件 */
    watch(
      () => widgetActive.activeWidget,
      widget => {
        emit('widget-active', widget)

        if (widget) state.activated = true
      }
    )

    /** 新增控件绑定合同控件 */
    viewer.$on('widget-add', (widget: Widget) => (widget.viewer = viewer))

    /** 组件销毁后控件取消绑定合同控件 */
    onUnmounted(() => {
      props.widgets.forEach(item => (item.viewer = null))
    })

    /** 控件点击聚焦 */
    viewer.$on('widget-click', (widget: Widget) => (widgetActive.activeWidget = widget))

    /** 控件拖入清除多选 */
    viewer.$on('widget-drop', () => selection.clear())

    return {
      /** 合同控件节点 */
      viewerEl,

      /** 当前文档 */
      doc,

      /** 骑缝控件列表(包含所有文档) */
      ridings,

      /** 当前文档骑缝控件列表 */
      docRidings,

      /** 骑缝区域宽度 */
      ridingWidth,

      /** 批量控件列表(包含所有文档) */
      batches,

      /** 文档状态 */
      state,

      /** 文档滚动 */
      scroll,

      /** 布局 */
      layout,

      /** 缩放 */
      zoom,

      /** 工具区域 */
      tools,

      /** 滚动节点 */
      scroller,

      /** 加载中图片 */
      loadingImage: config.loadingImage,

      /** 控件激活 */
      ...toRefs(widgetActive),

      /** 控件排序 */
      ...toRefs(widgetSort),

      /** 控件操作 */
      ...widgetOperate,

      /** 控件校验 */
      ...toRefs(widgetValidate),

      /** 控件数据来源 */
      source,

      /** 控件选择 */
      selection,

      /** 滚动事件 */
      onScroll(e: Event) {
        emit('scroll', e, scroll.pageIndex)
      }
    }
  }
})
</script>

<style lang="scss">
.fa-contract {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  background: #f1f1f1;

  &__viewer {
    position: relative;
    flex: auto;
    height: 100%;
  }

  &.has-toolbar &__viewer {
    height: calc(100% - $contract-toolbar-height);
  }

  &__scroll {
    width: 100%;
    height: 100%;
    overflow-y: scroll;
  }

  &__doc-wrapper {
    position: relative;
    width: $contract-doc-width;
    margin: 0 auto;
    padding: 0 $contract-doc-padding;
  }

  &__doc {
    position: absolute;
    top: 0;
    left: 50%;
    transform-origin: 50% 0%;
  }

  /** 工具栏 */
  &__tools {
    position: absolute;
    z-index: 1;

    .tools-wrapper {
      position: absolute;
      display: flex;
      flex-direction: column;
      align-items: center;
      width: 48px;

      &__left-top,
      &__right-top {
        top: $contract-doc-padding;
      }

      &__left-middle,
      &__right-middle {
        top: 50%;
        transform: translateY(-50%);
      }

      &__left-bottom,
      &__right-bottom {
        bottom: $contract-doc-padding;
      }
    }

    .tools-btn {
      flex: none;
      box-sizing: border-box;
      letter-spacing: 0;
      text-align: center;
      cursor: pointer;
      user-select: none;

      ~ .tools-btn {
        margin-top: 8px;
      }

      &.is-disabled {
        cursor: not-allowed;
      }
    }

    /** 相对滚动区域固定 */
    &.fixed-scroller.tools {
      &-left {
        top: 0;
        left: 0;
        .tools-wrapper {
          left: 0;
        }
      }

      &-right {
        top: 0;
        .tools-wrapper {
          right: 0;
        }
      }
    }

    /** 相对文档区域固定 */
    &.fixed-doc {
      left: 50%;

      &.tools {
        &-left,
        &-right {
          top: 0;
          bottom: 0;

          .tools-wrapper {
            right: 100%;
          }
        }

        &-right .tools-wrapper {
          left: 0;
        }
      }
    }
  }

  /** 回到顶部 */
  .btn-scroll {
    $s: 32px;
    @extend .fa-btn-light;

    display: block;
    height: $s;
    font-size: 18px;
    line-height: $s;
    box-shadow: none;

    &__wrapper {
      width: $s;
      background-color: #fff;
      border-radius: 4px;
      box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.1);
    }
  }
}
</style>
